<?php
/**
 * Copyright (c) 2006-2008, Julien PORTALIER
 * http://featherphp.googlecode.com/
 * 
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 */

/**
 * ACL resource accessor.
 */
class Aro extends ActiveRecord_Base
{
	protected $_has_many = array('Role');
	
	/**
	 * Finds ID of a given ARO.
	 * 
	 * @return Integer aro_id
	 * @param $aro String
	 */
	function resolve_id($aro)
	{
		$id = $this->find_field_by_aro($aro, 'id');
		return empty($id) ? null : $id;
	}
	
	/**
	 * Creates a new ARO.
	 * 
	 * @return Integer aro_id
	 * @param $aro String
	 */
	function register($aro)
	{
		$id = $this->resolve_id($aro);
		if ($id === null)
		{
			$Aro = new self(array('aro' => $aro));
			$Aro->create(false);
			return $Aro->id;
		}
		return $id;
	}
	
	/**
	 * Finds or creates an ARO.
	 * 
	 * @return Integer aro_id
	 * @param $aro String
	 */
	function resolve_id_or_register($aro)
	{
		$id = $this->resolve_id($aro);
		if (!$id) {
			$id = $this->register($aro);
		}
		return $id;
	}
}

/**
 * ACL resource.
 */
class Aco extends ActiveRecord_Base
{
	protected $_has_many = array('Role');
	
	/**
	 * Returns ID of a given ACO.
	 * 
	 * @return Integer aco_id
	 * @param $aco String
	 */
	function resolve_id($aco)
	{
		$id = $this->find_field_by_aco($aco, 'id');
		return empty($id) ? null : $id;
	}
	
	/**
	 * Creates a new ARO.
	 * 
	 * @return Integer aco_id
	 * @param $aco String
	 */
	function register($aco)
	{
		$id = $this->resolve_id($aco);
		if ($id === null)
		{
			$Aco = new self(array('aco' => $aco));
			$Aco->create(false);
			return $Aco->id;
		}
		return $id;
	}
	
	/**
	 * Finds or creates an ACO.
	 * 
	 * @return Integer aco_id
	 * @param $aco String
	 */
	function resolve_id_or_register($aco)
	{
		$id = $this->resolve_id($aco);
		if (!$id) {
			$id = $this->register($aco);
		}
		return $id;
	}
}


/**
 * ACL permission.
 */
class Role extends ActiveRecord_Base
{
	protected $_belongs_to = array('Aro', 'Aco');
	protected $_cache      = array();
	
	/**
	 * Returns a list of permissions for a given ressource.
	 * 
	 * @return Array
	 * @param $aco    String
	 * @param $aro    String[optional]
	 * @param $action String[optional]
	 */
	function & find_permissions($aco, $aro=null, $action='*')
	{
		$fields = ($action == '*') ? 'Role.read,Role.write,Role.delete' : "Role.$action";
		
		$conditions = array('Aco.aco' => $aco);
		if ($aro !== null) {
			$conditions['Aro.aro'] = $aro;
		}
		$options = array(
			'conditions' => &$conditions,
			'order'      => 'Aco.aco DESC',
			'include'    => array('Aro', 'Aco'),
			'process'    => DBO::PROCESS_LITE,
			'extend'     => false,
			'fields'     => "Aro.aro, Aco.aco, $fields"
		);
		$perms = $this->find_all(&$options);
		return $perms;
	}
	
	/**
	 * Returns role's id for a given aro and aco.
	 * 
	 * @return Integer
	 * @param $aro_id Integer
	 * @param $aco_id Integer
	 */
	function resolve_id($aro_id, $aco_id)
	{
		$options = array('conditions' => array(
			'aro_id' => $aro_id,
			'aco_id' => $aco_id
		));
		$id = $this->find_field('Role.id', &$options);
		return empty($id) ? null : $id;
	}
	
	/**
	 * Returns a a list of role ids for a given aco (and aro).
	 * 
	 * @return Array 
	 * @param $aco String[optional]
	 * @param $aro String[optional]
	 */
	function resolve_ids($aco=null, $aro=null)
	{
		if (isset($aco) or isset($aco))
		{
			$options = array(
				'fields'     => 'Role.id',
				'conditions' => array(),
				'include'    => array(),
				'process'    => DBO::PROCESS_LIST,
			);
			if ($aco !== null)
			{
				$options['conditions']['Aco.aco'] = $aco;
				$options['include'][] = 'Aco';
			}
			if ($aro !== null)
			{
				$options['conditions']['Aro.aro'] = $aro;
				$options['include'][] = 'Aro';
			}
			return $this->find(':all', &$options);
		}
		return false;
	}
	
	/**
	 * 
	 * @param $aro    String
	 * @param $aco    String
	 * @param $read   Boolean
	 * @param $write  Boolean
	 * @param $delete Boolean
	 */
	function register($aro, $aco, $read, $write, $delete)
	{
		$Role = new self();
		
		# rights
		if ($read   !== null) $Role->read   = $read;   else unset($Role->read);
		if ($write  !== null) $Role->write  = $write;  else unset($Role->write);
		if ($delete !== null) $Role->delete = $delete; else unset($Role->delete);
		
		# ids
		$aro_id   = $this->Aro->resolve_id_or_register($aro);
		$aco_id   = $this->Aco->resolve_id_or_register($aco);
		$Role->id = $this->resolve_id($aro_id, $aco_id);
		
		# save
		if ($Role->id) {
			$Role->update($Role->id);
		}
		else
		{
			$Role->aro_id = $aro_id;
			$Role->aco_id = $aco_id;
			unset($Role->id);
			
			$Role->create();
		}
	}
	
	/**
	 * Destroys roles associated to a given aco.
	 * 
	 * @param $aco String[optional]
	 * @param $aro String[optional]
	 */
	function destroy($aco=null, $aro=null)
	{
		$rs = $this->resolve_ids($aco, $aro);
		if (!empty($rs))
		{
			$conditions = array('id' => &$rs);
			return $this->_db->delete($this->_table, $conditions);
		}
	}
}

/**
 * Access Control Lists.
 */
class Acl
{
	private $_cache = array();
	
	function __get($attr)
	{
		if ($attr == 'Role') {
			return $this->Role = new Role();
		}
	}
	
	function startup(Controller $controller)
	{
		$this->Auth = $controller->Auth;
	}
	
	/**
	 * Checks if an ARO has rights overs over an ACO.
	 * 
	 * @return mixed Returns true if explicitly authorized; false if explicitly unauthorized, and null otherwise.
	 * @param $aco String
	 * @param $aro String[optional]
	 * @param $action String[optional] read, write or delete
	 */
	function check($aco, $aro=null, $action='read')
	{
		$cache_id = "$aco#$aro";
		if (isset($this->_cache[$cache_id])) {
			$perms =& $this->_cache[$cache_id];
		}
		else
		{
			# permits wildcards
			if ($aco != '*')
			{
				$acos = array($aco);
				if (preg_match('/^([^.]+)\.[^\*]+$/', $aco, $match)) {
					$acos[] = "{$match[1]}.*";
				}
				$acos[] = '*';
			}
			
			# gets permissions
			$perms = $this->Role->find_permissions(isset($acos) ? $acos : '*', $aro);
			$this->_cache[$cache_id] =& $perms;
		}
		
		if (!empty($perms))
		{
			
			# 1. checks if member is explicitly authorized
			foreach($perms as $perm)
			{
				if ($perm['aro'] == "Member.{$this->Auth->id}") {
					return $perm[$action];
				}
			}
			
			# 2. falls back on checking rights for member's groups
			$groups = array('Group.all');
			foreach($this->Auth->groups as $g) {
				$groups[] = "Group.$g";
			}
			
			foreach($perms as $perm)
			{
				if (in_array($perm['aro'], $groups))
				{
					if ($perm[$action]) {
						return true;
					}
					$rs = false;
				}
			}
		}
		return isset($rs) ? $rs : null;
	}
	
	/**
	 * Grants a right.
	 * 
	 * \code
	 * $Acl->grant("Group.administrator", "*");
	 * $Acl->grant("Group.moderator",     "Forum.*", "read,write");
	 * $Acl->grant("Group.member",        "Forum.*", "read");
	 * \endcode
	 * 
	 * @param $aro String
	 * @param $aco Object
	 * @param $action String[optional] Either read, write, delete, * or a combinaison of these.
	 * @param $value Boolean[optional]
	 */
	function grant($aro, $aco, $action='*', $value=true)
	{
		if ($action == '*')
		{
			$read   = $value;
			$write  = $value;
			$delete = $value;
		}
		else
		{
			foreach(explode(',', $action) as $action)
			{
				$action  = trim($action);
				$$action = $value;
			}
		}
		$this->Role->register($aro, $aco,
			isset($read)   ? $read   : null,
			isset($write)  ? $write  : null,
			isset($delete) ? $delete : null
		);
	}
	
	/**
	 * Revokes a right.
	 * 
	 * @param $aro String
	 * @param $aco String
	 * @param $action String[optional] Either read, write, delete, * or a combinaison of these.
	 */
	function revoke($aro, $aco, $action='*')
	{
		$this->grant($aro, $aco, $action, false);
	}
	
	/**
	 * Alias for Acl::grant().
	 */
	function allow($aro, $aco, $action='*')
	{
		$this->grant($aro, $aco, $action, true);
	}
	
	/**
	 * Alias for Acl::revoke().
	 */
	function deny($aro, $aco, $action='*')
	{
		$this->grant($aro, $aco, $action, false);
	}
	
	/**
	 * Fully removes a Role and all its associated rights.
	 * 
	 * @param $aco String
	 * @param $aro String[optional]
	 */
	function destroy($aco, $aro=null)
	{
		$this->Role->destroy($aco, $aro);
	}
	
	/**
	 * Creates a new ARO.
	 * 
	 * @param $aro String
	 */
	function register_aro($aro)
	{
		$this->Role->Aro->register($aro);
	}
}

/**
 * Component derivation of ACL.
 */
class AclComponent extends Acl {
	
}

?>